/*
 *  Blazon: A user datagram protocol re-braodcasting daemon.
 *  config.c: Configuration file and data handling.
 *
 *  Copyright 2012 Blazon development team
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.

 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "std-include.h"

#include <netinet/in.h>	/* inet_ntop() */
#include <arpa/inet.h>	/* inet_ntop() */
#include <stdlib.h>		/* strtol() */
#include <limits.h>		/* strtol() */
#include <stddef.h>		/* offsetof() */
#include <regex.h>

#include "tools.h"
#include "base-io.h"
#include "blazon.h"
#include "config.h"
#include "log.h"
#include "match.h"

int re_flags = REG_ICASE | REG_NOSUB | REG_NEWLINE | REG_EXTENDED;
regex_t game_regexp;
const char *game_pattern = "^[a-z0-9 :-_\\.)(]+,[ ]*[0-9]{4,5}(,[ ]*[0-9]{1,3})+$";


/* external functions */
bool load_config(struct config_file **);
void destroy_config(struct config_file *);
void write_config_file(struct config_file *);

static bool sanity_check_config(struct config_file *);
static void populate_array(int8_t, struct config_file *);
static bool add_game_entry(struct config_file *, char *, char *);
static bool add_subnet_entry(struct config_file *, char *, char *);
static bool add_conf_entry(struct config_file *, char *, char *);
static bool add_entry(struct config_file *, char *, char *, int);
static void free_subnet(struct subnet_info *);
static void free_game(struct game_info *);
static uint8_t match_entry_type(const char *);
static dlink_node * find_duplicate_game(struct config_file *, uint16_t);
static dlink_node * find_duplicate_subnet(struct config_file *, const char *);
static bool read_config_file(struct config_file *);
static bool parse_line(char *, char *, char *);

void
config_init(void)
{
	if (regcomp(&game_regexp, game_pattern, re_flags) != 0)
		die("Error: game_regexp failed to compile");
}

void
config_cleanup(void)
{
	regfree(&game_regexp);
}

static void
set_config_defaults(struct config_file *conf)
{
	if (!conf)
		return;

	/* Config defaults should only ever be applied to a new config_file
	 * struct and should therefore never need free()ing here. */
	conf->listen = my_strdup("*");
	conf->pidfile = my_strdup("blazon.pid");
	conf->statsfile = my_strdup("game-stats.txt");
	conf->logfile = my_strdup("/var/log/blazon/blazon.log");
	conf->loglevel = 3;
	conf->stats_freq = 5;
	conf->hashlog_freq = 20;
	conf->hashclean_freq = 12;
	conf->hashnode_life = 6;
}

/* ---------------------------------------------------------------- */

static struct table_entry config_table[] =
{
	{ "listen", CONF_TYPE_STR, offsetof(struct config_file, listen)	},
	{ "pidfile", CONF_TYPE_STR, offsetof(struct config_file, pidfile)	},
	{ "statsfile", CONF_TYPE_STR, offsetof(struct config_file, statsfile)	},
	{ "logfile", CONF_TYPE_STR, offsetof(struct config_file, logfile)	},
	{ "loglevel", CONF_TYPE_INT, offsetof(struct config_file, loglevel)	},
	{ "stats_freq", CONF_TYPE_INT, offsetof(struct config_file, stats_freq)	},
	{ "hashlog_freq", CONF_TYPE_INT, offsetof(struct config_file, hashlog_freq)	},
	{ "hashclean_freq", CONF_TYPE_INT, offsetof(struct config_file, hashclean_freq)	},
	{ "hashnode_life", CONF_TYPE_INT, offsetof(struct config_file, hashnode_life)	},
	{ "\0", 0, 0 }
};

/* ---------------------------------------------------------------- */

/* TODO: Remove when no longer needed for debugging. */
void
write_config_file(struct config_file *conf)
{
    FILE *fb;
    char buf[512];
    char lensbuf[32];
	int i;
	struct game_info *game_p;
	dlink_node *ptr;

    if((fb = fopen("conf-output", "a")) != NULL)
    {

        fputs("\n\n\n----------\n blazon configuration\n----------\n", fb);

		for (i = 0; config_table[i].type; i++)
		{
			if (config_table[i].type == CONF_TYPE_STR)
			{
        		snprintf(buf, sizeof(buf), "%s = %s\n",
						config_table[i].name, *((char **)(void *)((char *)conf + config_table[i].cfg_off)) );
			}
			else if (config_table[i].type == CONF_TYPE_INT)
			{
        		snprintf(buf, sizeof(buf), "%s = %d\n",
						config_table[i].name, *((int *)(void *)((char *)conf + config_table[i].cfg_off)) );
			}

        	if(fputs(buf, fb) == -1)
				log_debug("Error writing config entry %s (%s)",
						"conf-output", strerror(errno));
		}


		DLINK_FOREACH(ptr, conf->game_list.head)
		{
			game_p = ptr->data;

			lensbuf[0] = '\0';
			if (game_p->lens_size > 0)
			{
				snprintf(lensbuf, sizeof(lensbuf), "%d", game_p->lens[0]);
				for (i = 1; i < game_p->lens_size; i++)
					snprintf(lensbuf, sizeof(lensbuf), "%s, %d", lensbuf, game_p->lens[i]);
			}
			snprintf(buf, sizeof(buf), "game = %s (%d) :%s [%d]. game_p: %p, lens_p: %p, fd_ptr: %p, sentcount: %d, throtcount: %d\n",
					game_p->name, game_p->port, lensbuf, game_p->lens_size,
					game_p, game_p->lens, game_p->fd_ptr, game_p->sentcount,
					game_p->throtcount);
			if(fputs(buf, fb) == -1)
				log_debug("Error writing games %s (%s)",
						"conf-output", strerror(errno));
		}


		for(i = 0; i < conf->subnets_size; i++)
		{
			snprintf(buf, sizeof(buf), "subnet = %s, %s\n",
					conf->subnets[i].subnet, conf->subnets[i].broadcast);
			if(fputs(buf, fb) == -1)
				log_debug("Error writing subnets %s (%s)",
						"conf-output", strerror(errno));
		}


        fflush(fb);
        fclose(fb);
    }
    else
        log_debug("Error opening output config file: %s", "config.output");
}


bool
load_config(struct config_file **conf)
{
	/* Initialise regexp structs */
	config_init();

	if (*conf)
	{
		log_err("load_config(): config pointer is not NULL.");
		return false;
	}

	*conf = my_malloc(sizeof(struct config_file));

	set_config_defaults(*conf);

	if (!read_config_file(*conf))
	{
		destroy_config(*conf);
		*conf = NULL;
		return false;
	}

	if (!sanity_check_config(*conf))
	{
		destroy_config(*conf);
		*conf = NULL;
		return false;
	}

	/* Free regexp structs */
	config_cleanup();

	/* Leave copying game/subnet entries to their arrays until last.
	 * This way, we don't waste time copying stuff if a part of
	 * the config is not valid.
	 */
	populate_array(CONF_TYPE_SUBNET, *conf);

	return true;
}

/* ---------------------------------------------------------------- */

/* This function assumes the contiguous game/subnet array does not
 * already exist and creates it.
 * Side effects:
 */
static void
populate_array(int8_t type, struct config_file *conf_p)
{
	int i;
	struct subnet_info *sub_ptr;
	dlink_node *ptr, *next_ptr;

	if (type == CONF_TYPE_SUBNET)
	{
		conf_p->subnets = my_calloc(dlink_list_length(&conf_p->subnet_list),
				sizeof(struct subnet_info));
		conf_p->subnets_size = dlink_list_length(&conf_p->subnet_list);

		i = 0;
		DLINK_FOREACH_SAFE(ptr, next_ptr, conf_p->subnet_list.head)
		{
			sub_ptr = ptr->data;

			strlcpy(conf_p->subnets[i].subnet, sub_ptr->subnet,
					sizeof(conf_p->subnets[i].subnet));
			strlcpy(conf_p->subnets[i].broadcast, sub_ptr->broadcast,
					sizeof(conf_p->subnets[i].broadcast));

			dlink_destroy(ptr, &conf_p->subnet_list);
			free_subnet(sub_ptr);

			i++;
		}
	}
}

/* ---------------------------------------------------------------- */

static bool
add_game_entry(struct config_file *conf, char *name, char *value)
{
	char *name_p, *token, *str = value;
	long number;
	int i, comma_count;
	size_t str_len;
	char buf[256];
	struct game_info *game_p;
	dlink_node *node;

	int rtnExec = regexec(&game_regexp, value, 0, NULL, 0);

	if (rtnExec != 0)
	{
		log_debug("Regexp check failed (%d)", rtnExec);
		return false;
	}

	/* How long is the value */
	str_len = strnlen(value, CONF_VALUE_LEN);
	
	/* Count how many commas are in the string. */
	comma_count = 0;
	for (i = 0; i < str_len; i++)
	{
		if (value[i] == ',')
			comma_count++;
	}

	/* Grab the start of the game's name */
	name_p = strsep(&str, ",");

	/* Grab the port number used by this game. */
	token = strsep(&str, ",");
	number = strtol(token, NULL, 10);
	/* Check the port number is valid. */
	if (number <= 1024 || number > 65535)
	{
		log_err("Bad port number");
		return false;
	}

	/* Now we know the port number, check for a duplicate.
	 * Make sure <number> is a valid uint16_t value.
	 */
	if ((node = find_duplicate_game(conf, number)) != NULL)
	{
		game_p = node->data;
		log_warn("Overwriting duplicate game: %s, port: %d",
				game_p->name, game_p->port);
		my_free(game_p->name);
		my_free(game_p->lens);
	}
	else
	{
		game_p = my_malloc(sizeof(struct game_info));
		dlink_add_alloc(game_p, &conf->game_list);
	}

	game_p->name = my_strdup(name_p);
	game_p->lens = my_calloc(comma_count-1, sizeof(uint16_t));
	game_p->lens_size = comma_count-1;
	game_p->port = number;


	/* Fetch the packet lengths and store them in the pktlens array. */
	i = 0;
	while ((token = strsep(&str, ",")) != NULL)
	{
		number = strtol(token, NULL, 10);
		if (number > -1 && number < 999)
			game_p->lens[i] = number;
		else
			game_p->lens[i] = 0;
		i++;
	}

	/* TODO: Remove when no longer needed for debugging. */
	/*buf[0] = '\0';
	snprintf(buf, sizeof(buf), "%d", game_p->lens[0]);
	for (i = 1; i < game_p->lens_size; i++)
		snprintf(buf, sizeof(buf), "%s, %d", buf, game_p->lens[i]);
	log_info("Adding game: %s (%d) :%s [%d]",
			game_p->name, game_p->port, buf, game_p->lens_size);*/

	return true;
}

/* ---------------------------------------------------------------- */

static bool
add_subnet_entry(struct config_file *conf, char *name, char *value)
{
	char *cidr_ptr, *ip_ptr;
	char *token, *str = value;
	struct subnet_info *sub_ptr;
	dlink_node *dup;

	cidr_ptr = ip_ptr = NULL;

	while ((token = strsep(&str, ",")) != NULL)
	{
		while (*token == ' ' && token != str)
			token++;
		if (valid_cidr_mask(token))
		{
			cidr_ptr = token;
			break;
		}
	}

	while ((token = strsep(&str, ",")) != NULL)
	{
		while (*token == ' ' && token != str)
			token++;
		if (valid_ip_address(token))
		{
			ip_ptr = token;
			break;
		}
	}

	if ((cidr_ptr != NULL) && (ip_ptr != NULL))
	{
		/* Check for duplicate */
		if ((dup = find_duplicate_subnet(conf, cidr_ptr)) != NULL)
		{
			log_info("Overwriting duplicate subnet: %s", cidr_ptr);
			sub_ptr = dup->data;
		}
		else
		{
			log_debug("Adding subnet: %s, %s", cidr_ptr, ip_ptr);
			sub_ptr = my_malloc(sizeof(struct subnet_info));
			dlink_add_alloc(sub_ptr, &conf->subnet_list);
		}

		strlcpy(sub_ptr->subnet, cidr_ptr, sizeof(sub_ptr->subnet));
		strlcpy(sub_ptr->broadcast, ip_ptr, sizeof(sub_ptr->broadcast));

		return true;
	}
	return false;
}

/* ---------------------------------------------------------------- */

static bool
add_conf_entry(struct config_file *conf, char *name, char *value)
{
	int i;
	char *cptr;

	for(i = 0; config_table[i].type; i++)
	{
		if (strcasecmp(config_table[i].name, name) == 0)
		{
			switch (config_table[i].type)
			{
			case CONF_TYPE_INT:
				cptr = value;
				*((int *)(void *)((char *)conf + config_table[i].cfg_off))
						= (int) strtol(value, &cptr, 10);
				/*(*(int *)entry->cfg_ptr) = entry->intval;
				(*entry)->cfg_ptr = config_table[i].cfg_ptr;*/

				/* Checks this was recognised as a valid number. */
				if (value != cptr && *cptr == '\0')
				{
					return true;
				}
				break;

			case CONF_TYPE_BOOL:
				/*(*(bool *)entry->cfg_ptr) = entry->boolval;
				(*entry)->cfg_ptr = config_table[i].cfg_ptr;*/
				if (!strncasecmp(value, "on", 2) ||
					!strncasecmp(value, "yes", 3) ||
					!strncasecmp(value, "true", 4))
				{
					*((bool *)(void *)((char *)conf +
							config_table[i].cfg_off)) = true;
					return true;
				}
				else if (!strncasecmp(value, "off", 3) ||
					!strncasecmp(value, "no", 2) ||
					!strncasecmp(value, "false", 5))
				{
					*((bool *)(void *)((char *)conf +
							config_table[i].cfg_off)) = false;
					return true;
				}
				break;

			case CONF_TYPE_STR:
				my_free( *((char **)(void *)((char *)conf + config_table[i].cfg_off)) );
				*((char **)(void *)((char *)conf +
						config_table[i].cfg_off)) = my_strdup(value);
				/*(*(char **)entry->cfg_ptr) = my_strdup(entry->value);
				(*entry)->cfg_ptr = config_table[i].cfg_ptr;*/
				return true;
				break;
			}
		}
	}

	return false;
}

/* ---------------------------------------------------------------- */
/* Add entry to config, but check for validity first. */

static bool
add_entry(struct config_file *conf, char *name, char *value, int linenum)
{
	uint8_t type;

	/* Check if it's something we can recognise. */
	if ((type = match_entry_type(name)) == 0)
	{
		log_warn("Unknown option '%s' at line %d.", name, linenum);
		return false;
	}

	switch (type)
	{
		case CONF_TYPE_SUBNET:
			if (!add_subnet_entry(conf, name, value))
			{
				log_warn("Subnet entry at line %d is invalid.", linenum);
				return false;
			}
			break;
		case CONF_TYPE_GAME:
			if (!add_game_entry(conf, name, value))
			{
				log_warn("Game entry at line %d is invalid.", linenum);
				return false;
			}
			break;
		case CONF_TYPE_INT:
		case CONF_TYPE_BOOL:
		case CONF_TYPE_STR:
			if (!add_conf_entry(conf, name, value))
			{
				log_warn("Option '%s' at line %d has invalid type.",
						name, linenum);
				return false;
			}
			break;
	}

	return true;
}

/* ---------------------------------------------------------------- */

static bool
sanity_check_config(struct config_file *conf_p)
{
	/*struct conf_entry *entry;
	int subnet_c, game_c;
	dlink_node *ptr;*/

	if (dlink_list_length(&conf_p->game_list) == 0)
	{
		log_err("No valid game types present in config.");
		return false;
	}

	if (dlink_list_length(&conf_p->subnet_list) == 0)
	{
		log_err("No valid subnet types present in config.");
		return false;
	}

	if (conf_p->stats_freq < 0 || conf_p->stats_freq > 10080)
	{
		log_err("stats_freq setting must be in range 0 to 10080.");
		return false;
	}

	if (conf_p->hashlog_freq < 0 || conf_p->hashlog_freq > 10080)
	{
		log_err("hashlog_freq setting must be in range 0 to 10080.");
		return false;
	}

	if (conf_p->hashclean_freq < 0 || conf_p->hashclean_freq > 48)
	{
		log_err("hashclean_freq setting must be in range 0 to 48.");
		return false;
	}

	if (conf_p->hashnode_life < 1 || conf_p->hashnode_life > 48)
	{
		log_err("hashnode_life setting must be in range 1 to 48.");
		return false;
	}

	if (conf_p->loglevel < 1 || conf_p->loglevel > 4)
	{
		log_err("loglevel setting must be in range 1 to 4.");
		return false;
	}
	/* count how many subnets we'll be adding */
	/*subnet_c = game_c = 0;
	DLINK_FOREACH(ptr, config_list.head)
	{
		entry = ptr->data;
		if (entry->type == CONF_TYPE_SUBNET)
			subnet_c++;
		else if (entry->type == CONF_TYPE_GAME)
			game_c++;
	}*/


	return true;
}

/* ---------------------------------------------------------------- */

static void
free_subnet(struct subnet_info *snet)
{
	if (snet == NULL)
		return;
	my_free(snet);
}

/* ---------------------------------------------------------------- */

static void
free_game(struct game_info *game)
{
	if (game == NULL)
		return;
	my_free(game->name);
	my_free(game->lens);
	my_free(game);
}

/* ---------------------------------------------------------------- */

void
destroy_config(struct config_file *conf_p)
{
	int i;
	struct game_info *game;
	struct subnet_info *subnet;
	dlink_node *ptr, *next_ptr;

	if (!conf_p)
		return;

	/* Free any strings stored by conf options */
	for (i = 0; config_table[i].type; i++)
	{
		if (config_table[i].type == CONF_TYPE_STR)
		{
			my_free( *((char **)(void *)((char *)conf_p +
					config_table[i].cfg_off)) );
			/*(*(char **)(void *)((char *)conf_p +
					config_table[i].cfg_off)) = NULL;*/
		}
	}

	DLINK_FOREACH_SAFE(ptr, next_ptr, conf_p->game_list.head)
	{
		game = ptr->data;

		dlink_destroy(ptr, &conf_p->game_list);
		free_game(game);
	}

	DLINK_FOREACH_SAFE(ptr, next_ptr, conf_p->subnet_list.head)
	{
		subnet = ptr->data;

		dlink_destroy(ptr, &conf_p->subnet_list);
		free_subnet(subnet);
	}

	/*if (conf_p->games)
	{
		for (i = 0; i < conf_p->games_size; i++)
		{
			my_free(conf_p->games[i].name);
			my_free(conf_p->games[i].lens);
		}
		my_free(conf_p->games);
	}*/

	my_free(conf_p->subnets);
	my_free(conf_p);
}


/* ---------------------------------------------------------------- */

/* Checks to see if <name> matches a known config option type.
 * If a match is found the type is returned. 
 */
static uint8_t
match_entry_type(const char *name)
{
	int i;

	if (strcasecmp(name, "subnet") == 0)
		return CONF_TYPE_SUBNET;
	if (strcasecmp(name, "game") == 0)
		return CONF_TYPE_GAME;

	for (i = 0; config_table[i].type; i++)
	{
		if (strcasecmp(config_table[i].name, name) == 0)
			return config_table[i].type;
	}
	return 0;
}

/* ---------------------------------------------------------------- */

static dlink_node *
find_duplicate_game(struct config_file *conf, uint16_t port)
{
	struct game_info *game_ptr;
	dlink_node *ptr;

	DLINK_FOREACH(ptr, conf->game_list.head)
	{
		game_ptr = ptr->data;

		if (game_ptr->port == port)
			return ptr;
	}

	return NULL;
}

/* ---------------------------------------------------------------- */

static dlink_node *
find_duplicate_subnet(struct config_file *conf, const char *subnet_str)
{
	struct subnet_info *subn_ptr;
	dlink_node *ptr;

	DLINK_FOREACH(ptr, conf->subnet_list.head)
	{
		subn_ptr = ptr->data;

		if (strcasecmp(subn_ptr->subnet, subnet_str) == 0)
			return ptr;
	}

	return NULL;
}

/* ---------------------------------------------------------------- */

static bool
read_config_file(struct config_file *conf)
{
	FILE *fp;
	bool ignore = false, conf_ok = true;
	int linenum = 0;
	char *line = NULL, name[CONF_NAME_LEN+1], value[CONF_VALUE_LEN+1];
	size_t linecap = CONF_LINE_LEN;
	ssize_t linelen;
	
	log_info("Reading config file: %s", CONF_FILENAME);
    fp = fopen(CONF_FILENAME, "r");

    if(!fp) {
		log_err("Could not open config file: %s", CONF_FILENAME);
        return false;
    }

	/* Allocate some memory for the line buffer */
	line = my_malloc(sizeof(char) * (CONF_LINE_LEN+1));

	/* getline() will realloc() the line buffer if it's too small. */
	while ((linelen = getline(&line, &linecap, fp)) > 0)
	{
        linenum++;
		if (linenum > 1024)
		{
			log_err("Config file is over 1024 lines. Bad configuration?");
			conf_ok = false;
			break;
		}

		if (line[0] == '\n' || line[0] == '#')
			continue;

		if (ignore) {
			if(!strncmp(line, "#*/", 3))
				ignore = false;
			continue;
        } else if (!strncmp(line, "#/*", 3)) {
			ignore = true;
			continue;
		}

		if (!parse_line(line, name, value))
			continue;

		if (!add_entry(conf, name, value, linenum))
			conf_ok = false;
	}

	fclose(fp);
	my_free(line);

	return conf_ok;
}


/* ---------------------------------------------------------------- */
/* Examines <line> to see whether it consists of a name/value pair.
 * If so, it sets <name> and <value> as strings and returns true.
 * Otherwise <name>/<value> are undefined and false is returned.
 */

static bool
parse_line(char *line, char *name, char *value)
{
	char *cptr, *walk;
	int i;

	/* Line must contain an equals sign. Store a ptr to the equals
	 * symbol so we know where to stop getting the entry name. */
	if ((cptr = strchr(line, '=')) == NULL) {
		return false;
	}

	walk = line;

	/* Ignore spaces left of the setting name */
	while (*walk == ' ' || *walk == '\t') {
		walk++;
	}

	/* Get the setting name. Stop when we reach the '=' symbol
	 * and ignores spaces we find. */
	/* TODO: Change this to check CONF_NAME_LEN? */
	i = 0;
	while (walk != cptr) {
		if (*walk != ' ' && *walk != '\t') {
			name[i] = *walk;
			i++;
		}
		walk++;
	}
	name[i] = '\0';

	if(EmptyString(name))
		return false;
	
	/* Ignore equal sign */
	if (*walk == '=') {
		walk++;
	}

	/* Ignore spaces left of the setting value */
	while (*walk == ' ' || *walk == '\t') {
		walk++;
	}

	/* Get the setting value */
	i = 0;
	/* TODO: Change this to check CONF_VALUE_LEN? */
	while (*walk != '\n') {
		if (*walk != '\t' && *walk != '\0') {
			value[i] = *walk;
			i++;
		}
		walk++;
	}
	value[i] = '\0';

	if(EmptyString(value))
		return false;

	return true;
}

